package Server;

import java.net.*;
import java.util.ArrayList;
import java.util.GregorianCalendar;
import java.util.Iterator;
import java.util.List;
import java.util.Queue;
import java.util.PriorityQueue;
import java.util.HashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.Date;
import java.io.*;
import java.sql.*;

import org.apache.xmlrpc.WebServer;
import org.apache.commons.*;

//import Server.PhoneServerThread.States;
import Server.PhoneServerThread.Status;
import Server.Tasks.*;

public class PhoneServer {
	
	private List<PhoneServerThread> phoneList = new CopyOnWriteArrayList<PhoneServerThread>();
	private List<Call> callList = new ArrayList<Call>();
	private Queue<ServerTask> messageList = new PriorityQueue<ServerTask>(1, new Task.TaskCompare());
	private HashMap<Integer, PhoneServerThread> phoneHash = new HashMap<Integer, PhoneServerThread>();
	private ServerSocket serverSocket = null;
	private PhoneListenThread conListen;
	private static boolean resetServer = false; 
	
	private int callLoad = 0;
	private int billingPeriod = 0;
	private long hwTestTimer = 0;
	private HWTestThread ht;
	private long billingTimer = 0;
	
	
	private class PhoneListenThread extends Thread {
		
		boolean killThread = false; 
		
	    public PhoneListenThread() {
	    	super("PhoneListenThread");
	    }
	    
	    public boolean end() {
	    	killThread = true;
	    	try {
				serverSocket.close();
			} catch (IOException e) {
				System.err.println("Error closing PhoneListenThread");
				return false;
			}
			return true;
	    }
	    
	    public void run() {
	    	
	    	try {
		        while (true) {
		        	PhoneServerThread t = new PhoneServerThread(serverSocket.accept(), PhoneServer.this);
		        	phoneList.add(t);
		        	t.start();
		        }
	    	} catch (IOException e) {
	    		if (killThread)
	    		System.err.println("ServerSocket has been closed");
	    		else
	    		{
	    			System.err.println("");
	    			System.exit(-1);
	    		}
	    		
	    	}
	    }
	}
	
	
	public PhoneServer() {
	
		System.out.println("PhoneServer booting up...");
		
		callLoad = PhoneServerDB.getPhoneServerDB().getCallLoad();
		System.out.println("System setting callLoad set to " + callLoad);
		billingPeriod = PhoneServerDB.getPhoneServerDB().getBillingPeriod();
		System.out.println("System setting billingPeriod set to " + billingPeriod + " minutes");
		
		ht = new HWTestThread(this, phoneList);
			
	}
    
	private int mainLoop() throws Exception
	{
		
        try {
            serverSocket = new ServerSocket(3000);
        } catch (IOException e) {
            System.err.println("Could not listen on port: 3000.");
            System.exit(-1);
        }

        conListen = new PhoneListenThread();
        conListen.start();
        BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in));
        
        WebServer server = null;
        
        // Start the RPC server
        try { 
			   // Invoke me as <http://localhost:3500/RPC2>.
		       server = new WebServer(3500);
		       server.addHandler("PhoneServer", this);
		       server.start();
		} catch (Exception exception) {
			   System.err.println("PhoneServer: " + exception.toString());
			   System.exit(-1);
		}
        	
		hwTestTimer = System.currentTimeMillis();
		billingTimer = System.currentTimeMillis();
		
        while (true) {
        	
        	/*if (stdIn.ready() && stdIn.readLine().equals("exit"))
        		break;
        */
        	if (stdIn.ready()) {
        		String input = stdIn.readLine();
        		if (input.equals("exit")) {
        			break;
        		} else if (input.equals("hwtest")) {
        			System.out.println("Hardware test result: " + conductHardwareTest("192.168.2.20"));
        		} else if (input.equals("reset")) {
        			resetSystem();
        		}
        	}
        	
        	ServerTask t = retrieveMessage();
        	if (t != null) {
        		t.ProcessTask();	
        	}
        	
        	checkRingTimeouts();
        	checkHWTestTimer();
        	checkBillingTimer();
        	
        	if (resetServer)
        	{
        		server.shutdown();
        		return 0;
        	}
        	//Update call logs
        	//...
        	
        }

        server.shutdown();
        conListen.end();
        return 1;
	}
	
	public synchronized ServerTask retrieveMessage()
	{
		if (!messageList.isEmpty())
			return messageList.poll();
		else
			return null;
	}
		
	public synchronized void addMessage(ServerTask t) {
		messageList.add(t);
	}
	
	public PhoneServerThread getExtension(int extension)
	{
		return phoneHash.get(extension);
	}
	
	public void addCallToCallList(Call call)
	{
		callList.add(call);
	}
	
	public void removeCallFromCallList(Call call)
	{
		callList.remove(call);
	}
			
	public void removePhone(PhoneServerThread t) {
		phoneList.remove(t);
	}
	
	public boolean checkCallLoad() {
		return (callList.size() < this.callLoad);
	}
	
	public void newCallLoad(int n) {
		
		System.out.println("Received new call load from Admin GUI.");
		this.callLoad = n;
		System.out.println("callLoad set to " + n + ".");
		balanceCallLoad();
	}
	
	public void newBillingPeriod(int p) {
		Date d = new Date();
		billingPeriod = p;
		billingTimer = System.currentTimeMillis();
		automaticBilling(d);	
	}
	
	private void balanceCallLoad() {
		
		System.out.println("Balancing call load...");
		
		Call c = null;
		
		while (callList.size() > this.callLoad) {
			c = callList.remove(0);
			disconnectCall(c);					
		}
		
		System.out.println("Finished balancing call load.");
		
	}
	
	public boolean conductHardwareTest(String ip) {
		
		PhoneServerThread p = null;
		
		System.out.println("Conducting Hardware Test on ip: "+ip);
		
		for (PhoneServerThread t : phoneList) {
			if (t.getPhoneName().equals(ip)) {
				p = t;
				break;
			}
		}
		
		if (p == null) 
			return false;
		
		//Run hardware test
		return p.hardwareTest();
	}
	
	public boolean resetSystem() {

		// Kill PhoneListenThread
		conListen.end();
					
		// Wait for HWTests
		try {
			ht.join();
		} catch (Exception e) {}
		
		// Prevent all phones from making calls
		for (PhoneServerThread t : phoneList)
		{
			t.sendMessage(new ResetTask(1, null));
		}
		
		Call c = null;
		
		// Cut all current calls
		while (callList.size() > 0) {
			c = callList.remove(0);
			disconnectCall(c);					
		}
		
		while(!phoneList.isEmpty()) {
			try{
				Thread.sleep(10);
			}
			catch (InterruptedException e)
			{
			}
		}

		// Kill PhoneServer
		resetServer = true;
		
		return true;
	}
	
	public void addedPhoneAccount(String ip) {
		
		PhoneServerThread p = null;
		
		for (PhoneServerThread t : phoneList) {
			if (t != null && t.getPhoneName().equals(ip)) {
				p = t;
			}
		}
		
		if (p != null) {
			getPhoneInfo(p);
		}
		
	}
	
	public void deactivatePhone(String ip) {
		
		PhoneServerThread p = null;
		
		for (PhoneServerThread t : phoneList) {
			if (t != null && t.getPhoneName().equals(ip)) {
				p = t;
			}
		}
		
		if (p != null) {
			
			Call c = p.getCurrentCall(); 
			if (c != null)
				disconnectCall(c);
			
			p.setPhoneStatus(PhoneServerThread.Status.DEACTIVATED);
			
		}
		
	}
	
	private void disconnectCall(Call c) {
		
		if (c != null) {
			System.out.println("Disconnecting call between " + c.getCallee().getExtension() + " and " + c.getCaller().getExtension());
			
			if (c.getState() == Call.State.RINGING) {
				c.getCaller().sendMessage(new EndRingingTask(2, null, c.getCaller()));
				c.getCallee().sendMessage(new EndRingingTask(2, null, c.getCallee()));
			} else {
				c.endCall();
				c.getCallee().sendMessage(new EndCallTask(2, null, c.getCallee()));
				c.getCaller().sendMessage(new EndCallTask(2, null, c.getCaller()));
			}
			
			c.getCaller().setCurrentCall(null);
			c.getCallee().setCurrentCall(null);
			
			callList.remove(c);
			c.CleanUp();
		
		}
		
	}
	
	private void checkRingTimeouts() {
		
		Call[] calls = callList.toArray(new Call[0]);
		
		for(Call c : calls) {
			if (c.getState() == Call.State.RINGING && c.ringTimeout()) {
				System.out.println("Ringing timeout exceeded, disconnecting call.");
				disconnectCall(c);
			}	
		}
		
		
	}
	
	private void checkHWTestTimer() {
		
		if ((System.currentTimeMillis() - hwTestTimer) >= 10000) {
			
			try {
				ht.join();
			} catch (Exception e) {}
			ht = new HWTestThread(this, phoneList);
			ht.start();
			hwTestTimer = System.currentTimeMillis();
		}
		
	}
	private void checkBillingTimer() {
		
		if ((System.currentTimeMillis() - billingTimer) >= (billingPeriod * 60000)) {
			Date d = new Date();
			billingTimer = System.currentTimeMillis();
			automaticBilling(d);	
		}
		
	}	
	
	public void checkPhone(PhoneServerThread p) {
		
		Iterator<PhoneServerThread> i = phoneList.iterator();
		
		while(i.hasNext()) {
			PhoneServerThread t = i.next();
			if (t != null && t != p && p.getPhoneName().equals(t.getPhoneName())) {
				try {
					t.disconnectPhone();
				} catch (IOException e) { 
					System.err.println("Error: Phone could not be disconnected");
				}
				i.remove();
			}
		}
		
	}
	
	public boolean changePhoneAccount(String old_ip, int old_extension, int old_permissions, String ip, int extension, int permissions) {
		
		PhoneServerThread old_phone = null;
		PhoneServerThread new_phone = null;
		
		for (PhoneServerThread p : phoneList) {
			if (p.getPhoneName().equals(old_ip)) 
				old_phone = p;
			if (p.getPhoneName().equals(ip))
				new_phone = p;
		}
		
		if (old_phone == null)
			return true;
		
		Call c = old_phone.getCurrentCall();
		if (c != null) {
			disconnectCall(c);
		}
		
		phoneHash.remove(old_extension);
		
		if (new_phone == null)
			return true;
		
		phoneHash.put(extension,new_phone);
		new_phone.setExtension(extension);
	
		boolean suspended = (old_phone.getPhoneStatus() == PhoneServerThread.Status.DEACTIVATED && old_permissions != 0);
		
		if (permissions == 0 || suspended) {
			new_phone.setPhoneStatus(PhoneServerThread.Status.DEACTIVATED);
		} else if (permissions == 1) {
			new_phone.setPhoneStatus(PhoneServerThread.Status.ACTIVATED);
		} else if (permissions == 2) {
			new_phone.setPhoneStatus(PhoneServerThread.Status.CANNOT_CALL);
		} else {
			new_phone.setPhoneStatus(PhoneServerThread.Status.CANNOT_RECEIVE);
		}
	
		return true;
	}

	
	//Retrieves phone information from the database
	public void getPhoneInfo(PhoneServerThread phone) {
		PhoneServerDB.getPhoneServerDB().getPhoneInfo(phone);
		phoneHash.put(phone.getExtension(), phone);
	} 
	
	private void automaticBilling(Date end_period) {
		
		System.out.println("Generating automatic bills...");
		
		List<Integer> phones = PhoneServerDB.getPhoneServerDB().getPhoneIds();
		Date start;
		for (Integer i : phones) {
			start = PhoneServerDB.getPhoneServerDB().getLastBillDate(i.intValue());
			generateBill(start, end_period, i.intValue());
		}
	}
	
	private void generateBill(Date start_date, Date end_date, int phone_id) {
		
		double amount;
		amount = PhoneServerDB.getPhoneServerDB().getCallRecordAmount(start_date, end_date, phone_id);
		PhoneServerDB.getPhoneServerDB().insertBill(phone_id, start_date, end_date, amount);
		
	}
	
	public static void main (String [] args) throws Exception
	{	
		PhoneServer pserv;
		int ret = 0;
		
		while(ret==0) {
			pserv = new PhoneServer();
			ret = pserv.mainLoop();
			resetServer = false;
		}
	}

}